﻿using Fluffy.Archive.Comparers;
using Fluffy.Archive.DragDropEngine;
using Fluffy.Library.DocumentContainer;
using Fluffy.Library.Mvvm;
using Fluffy.Library.Mvvm.Commands;
using Fluffy.Library.Utils;
using SevenZip;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shell;

namespace Fluffy.Archive
{
	public class ArchiveDocumentViewModel : DocumentViewModelBase
	{
		private readonly ArchiveDocumentModel _model;
		private readonly ArchiveEntityViewModel _rootFolder = new ArchiveEntityViewModel(new ArchiveFileInfo { FileName = "Root", IsDirectory = true });
		private readonly Command _openCommand;
		private readonly Command _copyCommand;
		private readonly Command _extractCommand;

		private IArchiveEntity _filesNames;
		private long _extractedBytes;
		private long _totalExtractionBytes;
		private IList _selectedItems;

		internal ArchiveDocumentViewModel(ArchiveDocumentModel model)
		{
			if (model == null) 
				throw new ArgumentNullException("model");

			_model = model;
			_openCommand = new Command(OnOpen, () => SelectedItems != null && SelectedItems.Count == 1 && !((IArchiveEntity)SelectedItems[0]).IsDirectory);
			_copyCommand = new Command(OnCopy, () => SelectedItems != null && SelectedItems.Count > 0);
			_extractCommand = new Command(OnExtract, () => SelectedItems != null && SelectedItems.Count > 0);

			// generate file tree
			List<ArchiveEntityViewModel> allEntities = _model.FilesNames.Select(x => new ArchiveEntityViewModel(x)).ToList();
			ArchiveEntityViewModel[] allFolders = allEntities.Where(x => x.IsDirectory).ToArray();

			Dictionary<string, ArchiveEntityViewModel> directoryByPath = allFolders.ToDictionary(x => x.FullFileName);

			for (int index = 0; index < allEntities.Count; index++)
			{
				ArchiveEntityViewModel item = allEntities[index];

				if (item.FullFileName.Contains(@"\"))
				{
					string directoryName = item.FullFileName.Substring(0, item.FullFileName.LastIndexOf(@"\", StringComparison.InvariantCulture));
					ArchiveEntityViewModel directory = GetDirectory(directoryName, directoryByPath);
					directory.Children.Add(item);

					if (item.IsDirectory)
					{
						item.Children.Insert(0, new RootFolderArchiveEntityViewModel(directory));
					}
				}
				else
				{
					_rootFolder.Children.Add(item);

					if (item.IsDirectory)
					{
						item.Children.Insert(0, new RootFolderArchiveEntityViewModel(_rootFolder));
					}
				}
			}

			SelectedFolder = _rootFolder;

			ProgressMessage = "Extracting...";
		}

		private ArchiveEntityViewModel GetDirectory(string directoryName, Dictionary<string, ArchiveEntityViewModel> directoryByPath)
		{
			ArchiveEntityViewModel directory;
			if (directoryByPath.TryGetValue(directoryName, out directory))
			{
				return directory;
			}

			directory = new ArchiveEntityViewModel(new ArchiveFileInfo { FileName = directoryName, IsDirectory = true });

			if (directoryName.Contains(@"\"))
			{
				ArchiveEntityViewModel parent = GetDirectory(directoryName.Substring(0, directoryName.LastIndexOf(@"\", StringComparison.InvariantCulture)), directoryByPath);
				parent.Children.Add(directory);

				directory.Children.Add(new RootFolderArchiveEntityViewModel(parent));
			}
			else
			{
				_rootFolder.Children.Add(directory);
				directory.Children.Add(new RootFolderArchiveEntityViewModel(_rootFolder));
			}

			directoryByPath.Add(directoryName, directory);
			return directory;
		}

		public string ArchiveName
		{
			get { return _model.ArchiveName; }
		}
		
		public long ExtractedBytes
		{
			get { return _extractedBytes; }
			set
			{
				_extractedBytes = value;
				ProgressValue = _extractedBytes/Convert.ToDouble(TotalExtractionBytes);
			}
		}

		public long TotalExtractionBytes
		{
			get { return _totalExtractionBytes; }
			set { _totalExtractionBytes = value; }
		}

		public ListCollectionView FilesNames
		{
			get
			{
				ListCollectionView lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(_filesNames.Children);
				lcv.CustomSort = new ArchiveContentNameComparer();
				return lcv;
			}
		}

		public IArchiveEntity SelectedFolder
		{
			get { return _filesNames; }
			set
			{
				if (_filesNames == value || value == null)
					return;

				_filesNames = value;
				OnPropertyChanged("SelectedFolder");
				OnPropertyChanged("FilesNames");

				_filesNames.IsSelected = true;
				_filesNames.IsExpanded = true;
			}
		}

		public IArchiveEntity[] RootFolderAsArray
		{
			get { return new IArchiveEntity[] { _rootFolder }; }
		}

		public IList<IArchiveEntity> Folders
		{
			get { return _rootFolder.Children.Where(x => x.IsDirectory && !x.IsRoot).ToArray(); }
		}

		public ICommand ExtractCommand
		{
			get { return _extractCommand; }
		}

		public override ICommand CutCommand
		{
			get { return new DisabledCommand(); }
		}

		public override ICommand CopyCommand
		{
			get { return _copyCommand; }
		}

		public ICommand OpenCommand
		{
			get { return _openCommand; }
		}

		public override ICommand PasteCommand
		{
			get { return new DisabledCommand(); }
		}

		public override ICommand SaveCommand
		{
			get { return new DisabledCommand(); }
		}

		public override ICommand SaveAsCommand
		{
			get { return new DisabledCommand(); }
		}

		public override CommandsContainer Menu1
		{
			get
			{
				CommandsContainer container = new CommandsContainer("Archive");
				container.Children.Add(new CommandItem("Open Selected Item", OpenCommand));
				container.Children.Add(new SeparatorItem());
				container.Children.Add(new CommandItem("Extract Selected Items...", ExtractCommand));
				container.Children.Add(new CommandItem("Extract All...", ExtractAllCommand));

				return container;
			}
		}

		public override CommandsContainer Menu2
		{
			get { return new CommandsContainer(); }
		}

		public override CommandsContainer Menu3
		{
			get { return new CommandsContainer(); }
		}

		public override void Close()
		{ }

		public ICommand ExtractForFileCommand
		{
			get { return new Command<IArchiveEntity>(OnExtract); }
		}

		public ICommand ExtractAllCommand
		{
			get { return new Command(OnExtractAll); }
		}

		public ICommand DragDropCommand
		{
			get { return new Command<IList>(DoDragDrop); }
		}

		public ICommand DragDropForFileCommand
		{
			get { return new Command<IArchiveEntity>(DoDragDrop); }
		}

		public ICommand OpenSelectedEntityCommand
		{
			get { return new Command<IArchiveEntity>(OpenEntity); }
		}

		private void OpenEntity(IArchiveEntity entity)
		{
			if (entity == null)
				return;

			if (entity.IsDirectory)
				OpenDirectory(entity);
			else
				OnOpen(entity);
		}

		public IList SelectedItems
		{
			get { return _selectedItems; }
			set
			{
				_selectedItems = value;
				_openCommand.RaiseCanExecuteChanged();
				_copyCommand.RaiseCanExecuteChanged();
				_extractCommand.RaiseCanExecuteChanged();
			}
		}

		private void OnExtract()
		{
			if (SelectedItems == null)
				return;

			OnExtract(SelectedItems);
		}

		private void OnExtract(IArchiveEntity item)
		{
			if (item == null)
				return;

			OnExtract(new[] { item });
		}

		private void OnExtract(IList files)
		{
			if (files.Count == 0)
				return;

			string dir = PathUtils.SelectDirectory("Select folder to extract the files.");

			if (string.IsNullOrEmpty(dir))
				return;

			ThreadUtils.StartThread(() =>
			{
				try
				{
					ProgressValue = 0;
					ProgressState = TaskbarItemProgressState.Normal;
					ExtractFiles(files.Cast<IArchiveEntity>().ToArray(), dir);
				}
				finally
				{
					ProgressState = TaskbarItemProgressState.None;
				}
			});
		}
		
		private void OnExtractAll()
		{
			if (_rootFolder.Children.Count == 0)
				return;

			string dir = PathUtils.SelectDirectory("Select folder to extract the files.");

			if (string.IsNullOrEmpty(dir))
				return;

			ThreadUtils.StartThread(() =>
			{
				try
				{
					ProgressValue = 0;
					ProgressState = TaskbarItemProgressState.Normal;
					ExtractFiles(_rootFolder.Children, dir);
				}
				finally
				{
					ProgressState = TaskbarItemProgressState.None;
				}
			});
		}

		private void ExtractFiles(IList<IArchiveEntity> items, string folder)
		{
			for (int marker = 0; marker < items.Count; marker++)
			{
				IArchiveEntity info = items[marker];

				if (info.IsDirectory)
				{
					ExtractFiles(info.Children.Where(x => !x.IsRoot).ToArray(), string.IsNullOrEmpty(folder) ? info.FileName : Path.Combine(folder, info.FileName));
				}
				else
				{
					_model.Extract(info.Info, folder);
				}
			}
		}
		
		private void OpenDirectory(IArchiveEntity item)
		{
			if (item == null || !item.IsDirectory)
				return;

			SelectedFolder = item;
		}

		private void DoDragDrop(IArchiveEntity entity)
		{
			if (entity == null)
				return;

			DoDragDrop(new[] { entity });
		}

		private void OnCopy()
		{
			if (SelectedItems == null)
				return;

			VirtualFileDataObject.FileDescriptor[] files = GetDescriptors(SelectedItems.Cast<IArchiveEntity>());
			VirtualFileDataObject virtualFileDataObject = new VirtualFileDataObject(
				e =>
				{
					ProgressValue = 0;
					ProgressState = TaskbarItemProgressState.Normal;
				}, 
				e =>
				{
					ProgressState = TaskbarItemProgressState.None;
				}) { IsAsynchronous = true };

			virtualFileDataObject.SetData(files);

			DoDragDropOrClipboardSetDataObject(MouseButton.Right, Application.Current.MainWindow, virtualFileDataObject, DragDropEffects.Move);
		}

		private void OnOpen()
		{
			IArchiveEntity entity = (IArchiveEntity)SelectedItems[0];
			OnOpen(entity);
		}

		private void OnOpen(IArchiveEntity entity)
		{
			ThreadUtils.StartThread(() =>
			{
				string tempDirectory = Path.GetTempPath();
				string tempFile = Path.Combine(tempDirectory, entity.FileName);

				try
				{
					ProgressValue = 0;
					ProgressState = TaskbarItemProgressState.Normal;
					ExtractFiles(new[] { entity }, tempDirectory);
					Process.Start(tempFile);
				}
				finally
				{
					ProgressState = TaskbarItemProgressState.None;
				}
			});
		}

		private void DoDragDrop(IList items)
		{
			VirtualFileDataObject.FileDescriptor[] files = GetDescriptors(items.Cast<IArchiveEntity>());
			VirtualFileDataObject virtualFileDataObject = new VirtualFileDataObject(
				e =>
				{
					ProgressValue = 0;
					ProgressState = TaskbarItemProgressState.Normal;
				},
				e =>
				{
					ProgressState = TaskbarItemProgressState.None;
				}) { IsAsynchronous = true };
			virtualFileDataObject.SetData(files);

			DoDragDropOrClipboardSetDataObject(MouseButton.Left, Application.Current.MainWindow, virtualFileDataObject, DragDropEffects.Move);
		}

		private VirtualFileDataObject.FileDescriptor[] GetDescriptors(IEnumerable<IArchiveEntity> items)
		{
			ExtractedBytes = 0;
			TotalExtractionBytes = 0;

			return items.SelectMany(x => GetDescriptors(x, string.Empty)).ToArray();
		}

		private IEnumerable<VirtualFileDataObject.FileDescriptor> GetDescriptors(IArchiveEntity info, string folder)
		{
			if (info.IsDirectory)
			{
				for (int index = 0; index < info.Children.Count; index++)
				{
					IArchiveEntity item = info.Children[index];
					if (item.IsRoot)
						continue;

					foreach (VirtualFileDataObject.FileDescriptor fileDescriptor in GetDescriptors(item, string.IsNullOrEmpty(folder) ? info.FileName : Path.Combine(folder, info.FileName)))
					{
						yield return fileDescriptor;
					}
				}

				yield break;
			}

			string fileName = Path.GetFileName(info.Info.FileName);
			long size = Convert.ToInt64(info.Info.Size);

			TotalExtractionBytes += size;

			Debug.Assert(!string.IsNullOrEmpty(fileName));
			
			ZipStream buffer = new ZipStream();
			ZipStreamWrapper zipStreamWrapper = new ZipStreamWrapper(buffer);
			zipStreamWrapper.SetLength(Convert.ToInt64(info.Info.Size));
			
			buffer.OnFirstRead += () => _model.ExtractIntoStreamAsync(info.Info, zipStreamWrapper);
			buffer.ProgressChanged += readBytes => ExtractedBytes += readBytes;
			
			VirtualFileDataObject.FileDescriptor descriptor = new VirtualFileDataObject.FileDescriptor();
			descriptor.Name = string.IsNullOrEmpty(folder) ? fileName : Path.Combine(folder, fileName);
			descriptor.StreamContents = buffer;
			descriptor.Length = size;
			
			yield return descriptor;
		}

		private static void DoDragDropOrClipboardSetDataObject(MouseButton button, DependencyObject dragSource, VirtualFileDataObject virtualFileDataObject, DragDropEffects allowedEffects)
		{
			try
			{
				if (button == MouseButton.Left)
				{
					// Left button is used to start a drag/drop operation
					VirtualFileDataObject.DoDragDrop(dragSource, virtualFileDataObject, allowedEffects);
				}
				else if (button == MouseButton.Right)
				{
					// Right button is used to copy to the clipboard
					// Communicate the preferred behavior to the destination
					virtualFileDataObject.PreferredDropEffect = allowedEffects;
					Clipboard.SetDataObject(virtualFileDataObject);
				}
			}
			catch (COMException)
			{
				// Failure; no way to recover
			}
		}
	}

	public interface IArchiveEntity
	{
		string FullFileName { get; }
		string FileName { get; }
		string SizeTitle { get; }
		ulong Size { get; }
		DateTime LastWriteTime { get; }
		string LastWriteTimeTitle { get; }
		ImageSource Icon { get; }
		string DisplayName { get; }
		bool IsDirectory { get; }
		bool IsRoot { get; }
		bool IsSelected { get; set; }
		bool IsExpanded { get; set; }
		ArchiveFileInfo Info { get; }
		List<IArchiveEntity> Children { get; }
	}

	public class RootFolderArchiveEntityViewModel : IArchiveEntity
	{
		private readonly IArchiveEntity _source;

		public RootFolderArchiveEntityViewModel(IArchiveEntity source)
		{
			_source = source;
		}

		public string FullFileName
		{
			get { return _source.FullFileName; }
		}

		public string FileName
		{
			get { return Path.GetFileName(_source.FullFileName); }
		}

		public ulong Size
		{
			get { return _source.Size; }
		}

		public string SizeTitle
		{
			get { return _source.SizeTitle; }
		}

		public DateTime LastWriteTime
		{
			get { return DateTime.MinValue; }
		}

		public string LastWriteTimeTitle
		{
			get { return string.Empty; }
		}

		public ImageSource Icon
		{
			get { return UiUtils.GetDirectoryIcon(false, true); }
		}

		public string DisplayName
		{
			get { return "..."; }
		}

		public bool IsDirectory
		{
			get { return true; }
		}

		public ArchiveFileInfo Info
		{
			get { return _source.Info; }
		}

		public List<IArchiveEntity> Children
		{
			get { return _source.Children; }
		}

		public bool IsSelected
		{
			get { return _source.IsSelected; }
			set { _source.IsSelected = value; }
		}

		public bool IsExpanded
		{
			get { return _source.IsExpanded; }
			set { _source.IsExpanded = value; }
		}

		public bool IsRoot
		{
			get { return true; }
		}
	}

	[DebuggerDisplay("{DisplayName}")]
	public class ArchiveEntityViewModel : ViewModelBase, IArchiveEntity
	{
		private readonly List<IArchiveEntity> _children = new List<IArchiveEntity>();
		private readonly ArchiveFileInfo _info;

		private bool _isSelected;
		private bool _isExpanded = true;

		public ArchiveEntityViewModel(ArchiveFileInfo info)
		{
			_info = info;
		}

		public string FullFileName
		{
			get { return _info.FileName; }
		}

		public string FileName
		{
			get { return Path.GetFileName(_info.FileName); }
		}

		public ulong Size
		{
			get
			{
				if (_info.IsDirectory) 
					return 0;

				return _info.Size;
			}
		}

		public string SizeTitle
		{
			get
			{
				if (_info.IsDirectory) 
					return string.Empty;

				return TitleUtils.GetTitleForSize(_info.Size);
			}
		}

		public DateTime LastWriteTime
		{
			get { return _info.LastWriteTime; }
		}

		public string LastWriteTimeTitle
		{
			get { return _info.LastWriteTime == DateTime.MinValue ? string.Empty : _info.LastWriteTime.ToString(CultureInfo.InvariantCulture); }
		}
		
		public ImageSource Icon
		{
			get
			{
				if (IsDirectory)
					return UiUtils.GetDirectoryIcon(false, false);

				return UiUtils.GetFileIcon(FullFileName, false);
			}
		}

		public string DisplayName
		{
			get
			{
				return _info.FileName.Contains(@"\") ? _info.FileName.Substring(_info.FileName.LastIndexOf('\\') + 1) : _info.FileName;
			}
		}

		public bool IsDirectory
		{
			get { return _info.IsDirectory; }
		}

		public bool IsSelected
		{
			get { return _isSelected; }
			set
			{
				if (_isSelected == value)
					return;

				_isSelected = value;
				OnPropertyChanged("IsSelected");
			}
		}

		public bool IsExpanded
		{
			get { return _isExpanded; }
			set
			{
				if (_isExpanded == value)
					return;

				_isExpanded = value;
				OnPropertyChanged("IsExpanded");
			}
		}

		public ArchiveFileInfo Info
		{
			get { return _info; }
		}

		public List<IArchiveEntity> Children
		{
			get { return _children; }
		}

		public IArchiveEntity[] Folders
		{
			get { return _children.Where(x => x.IsDirectory && !x.IsRoot).ToArray(); }
		}

		public bool IsRoot
		{
			get { return false; }
		}
	}
}
